﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.ComponentModel;
using System.IO;
using System.Diagnostics;
using System.Net;
using System.Threading;
using System.Text.RegularExpressions;

namespace YouTubeDownloader
{
    /// <summary>
    /// Provides methods for downloading videos from YouTube.
    /// </summary>
    public class Downloader
    {
        #region consts
        private const int BUFFER_SIZE = 4096;
        #endregion

        #region private fields
        private CookieContainer cookies = new CookieContainer();
        #endregion

        #region public properties
        /// <summary>
        /// Gets the status of the downloader, busy when downloading/
        /// </summary>
        public bool IsBusy { get; private set; }
        #endregion

        #region events

        public event EventHandler<DownloadVideoCompletedEventArgs> DownloadVideoCompleted;
        public event EventHandler<DownloadVideoProgressChangedEventArgs> DownloadVideoProgressChanged;

        #endregion

        #region constructor(s)

        public Downloader()
        {
        }

        #endregion

        #region main Download Loops

        private void Download(string videoId, string destinationFolder, bool async)
        {
            if (string.IsNullOrEmpty(destinationFolder))
                destinationFolder = Directory.GetCurrentDirectory();

            var responseRaw = GetResponseString(string.Format("http://www.youtube.com/watch?v={0}&hl=en_US&persist_hl=0", videoId));

            var title = Regex.Match(responseRaw, Regex.Escape(@"<meta name=""title"" content=""") + "(?<title>.*)\">")
                .Groups["title"].Value;

            var fileName = Path.Combine(destinationFolder, Path.ChangeExtension(title, ".mp4"));
            var partialFileName = string.Format(@"{0}.partial", fileName);
            Directory.CreateDirectory(fileName);

            var urlRaw = Regex.Match(responseRaw, @"""url\=(?<url>[^,]*),").Groups["url"].Value;
            var url = Uri.UnescapeDataString(urlRaw);
            url = url.Replace(@"\u0026", "&");
            var downloadUrl = url.Remove(url.IndexOf("type=")) + @"type=video/mp4";//;%20codecs=""avc1.64001F,%20mp4a.40.2""";

            var lastChangeTime = DateTime.Now;
            var buffer = new byte[BUFFER_SIZE];

            var response = GetResponse(downloadUrl);
            long totalBytesToDownload = response.ContentLength;
            long bytesDownloaded = 0;
            long prevBytesDownloaded = 0;
            DateTime checkTime = DateTime.Now;

            using (var outStream = File.Create(partialFileName, BUFFER_SIZE, FileOptions.SequentialScan))
            using (var inStream = response.GetResponseStream())
                while (bytesDownloaded < totalBytesToDownload)
                {
                    int r = inStream.Read(buffer, 0, BUFFER_SIZE);
                    outStream.Write(buffer, 0, r);
                    bytesDownloaded += r;

                    double secondsElapsed = (DateTime.Now - checkTime).TotalSeconds;
                    if (secondsElapsed > 0.5 || bytesDownloaded == totalBytesToDownload)
                    {
                        DownloadVideoProgressChangedEventArgs progressArgs = new DownloadVideoProgressChangedEventArgs()
                        {
                            Speed = (bytesDownloaded - prevBytesDownloaded) / secondsElapsed,
                            PercentageComplete = bytesDownloaded * 100.0 / totalBytesToDownload,
                            BytesDownloaded = bytesDownloaded,
                            TotalBytesToDownload = totalBytesToDownload
                        };

                        prevBytesDownloaded = bytesDownloaded;
                        checkTime = DateTime.Now;
                        if (async && DownloadVideoProgressChanged != null)
                            DownloadVideoProgressChanged.BeginInvoke(this, progressArgs, AsyncCallbackMethod, null);
                    }
                }

            if (File.Exists(fileName))
                File.Delete(fileName);
            File.Move(partialFileName, fileName);

            IsBusy = false;
            if (async && DownloadVideoCompleted != null)
                DownloadVideoCompleted.BeginInvoke(this, new DownloadVideoCompletedEventArgs() { FileName = fileName }, AsyncCallbackMethod, null);
        }
        private void AsyncDownload(object args)
        {
            var paramaters = (AsyncLoopParamaters)args;
            Download(paramaters.videoId, paramaters.destinationFolder, true);
        }

        #endregion

        #region public Download Methods

        /// <summary>
        /// Asynchronously downloads a video from YouTube into <paramref name="destinationFolder"/>.
        /// </summary>
        /// <param name="videoId">The unique YouTube video-id to download</param>
        /// <param name="destinationFolder">The path where the file will be created</param>
        public void DownloadVideoAsync(string videoId, string destinationFolder = null)
        {
            if (IsBusy) throw new ArgumentException("Cannot download while downloader is busy");
            IsBusy = true;

            Thread asyncThread = new Thread(new ParameterizedThreadStart(AsyncDownload));
            asyncThread.Start(new AsyncLoopParamaters() { videoId = videoId, destinationFolder = destinationFolder });
        }
        public void DownloadVideoAsync(Uri videoUri, string destinationFolder = null)
        {
            DownloadVideoAsync(GetVideoId(videoUri), destinationFolder);
        }
        /// <summary>
        /// Downloads a video from YouTube into <paramref name="destinationFolder"/>.
        /// </summary>
        /// <param name="videoId">The unique YouTube video-id to download</param>
        /// <param name="destinationFolder">The path where the file will be created</param>
        public void DownloadVideo(string videoId, string destinationFolder = null)
        {
            if (IsBusy) throw new ArgumentException("Cannot download while downloader is busy");
            IsBusy = true;

            Download(videoId, destinationFolder, false);
        }
        public void DownloadVideo(Uri videoUri, string destinationFolder = null)
        {
            DownloadVideo(GetVideoId(videoUri), destinationFolder);
        }
        #endregion

        #region private Helper methods

        private void AsyncCallbackMethod(IAsyncResult result) { }

        private WebResponse GetResponse(string url)
        {
            var request = WebRequest.Create(url) as HttpWebRequest;
            request.CookieContainer = cookies;
            return request.GetResponse();
        }

        private String GetResponseString(string url)
        {
            string result = string.Empty;
            using (StreamReader reader = new StreamReader(GetResponse(url).GetResponseStream()))
                result = reader.ReadToEnd();
            return result;
        }

        private string GetVideoId(Uri uri)
        {
            return Regex.Match(uri.Query, @"v=(?<videoId>[^\&]+)").Groups["videoId"].Value;
        }

        #endregion

        #region structs

        private struct AsyncLoopParamaters
        {
            public string videoId, destinationFolder;
        }

        #endregion
    }

    #region EventArgs

    /// <summary>
    /// Event Arguments for completion of a video download.
    /// </summary>
    public class DownloadVideoCompletedEventArgs : AsyncCompletedEventArgs
    {
        public DownloadVideoCompletedEventArgs() : base(null, false, null) { }
        public string FileName { get; internal set; }
    }

    /// <summary>
    /// Event Arguments for progress change on video download.
    /// </summary>
    public class DownloadVideoProgressChangedEventArgs : AsyncCompletedEventArgs
    {
        public DownloadVideoProgressChangedEventArgs() : base(null, false, null) { }
        public long BytesDownloaded { get; internal set; }
        public long TotalBytesToDownload { get; internal set; }
        public double PercentageComplete { get; internal set; }
        public double Speed { get; internal set; }
    }

    #endregion
}
